#include "deht.h"
#include <string.h>
#include <stdio.h>
#include <math.h>
#include <stdlib.h>
#include "../global/misc.h"
#include "../global/global_declarations.h"

/* DEHT key file structure:
 * header (DEHTpreferences)
 * "unrelated bytes"
 * pointer table - numEntriesInHashTable DEHT_FILE_PTRs
 * buckets content
 *
 * DEHT data file is just a blob of data pointed to by the key file
 */

DEHT_DISK_PTR start_of_ptr_table(DEHT* deht) {
	return sizeof(DEHTpreferences) + deht->header.numUnrelatedBytesSaved;
}

int write_null_bytes(int n, FILE* fp) {
	static unsigned char zeros_buffer[4096] = {0};
	int status, bytes_to_write;
	if (n <= 0) {
		return DEHT_STATUS_NOT_NEEDED;
	}
	while (n != 0) {

		if (n > 4096) {
			bytes_to_write = 4096;
			n -= 4096;
		}
		else {
			bytes_to_write = n;
			n = 0;
		}
		status = fwrite(zeros_buffer, 1, bytes_to_write, fp);
		if (status != bytes_to_write) {
			return DEHT_STATUS_FAIL;
		}
	}
	return DEHT_STATUS_SUCCESS;
}

static void flush_DEHT_files(DEHT* deht);

void write_seeds(DEHT* d, FILE* fp, int num_of_seeds) {
	int i;
	int64 seed;
	d->seeds = calloc(num_of_seeds, sizeof(int64));

	for (i = 0; i < num_of_seeds; i++) {
		seed = i*13 + 7;
		d->seeds[i] = seed;
		fwrite(&seed, sizeof(int64), 1, fp);
	}
}

void read_seeds(DEHT* d) {
	int num_of_seeds = d->header.numUnrelatedBytesSaved / sizeof(int64);
	if (num_of_seeds == 0) {
		d->seeds = 0;
		return;
	}
	d->seeds = calloc(num_of_seeds, sizeof(int64));
	fseek(d->keyFP, sizeof(DEHTpreferences), SEEK_SET);
	fread(d->seeds, sizeof(int64), num_of_seeds, d->keyFP);
}

DEHT *create_empty_DEHT(const char *prefix,/*add .key and .data to open two files return NULL if fail creation*/
                        hashKeyIntoTableFunctionPtr hashfun, hashKeyforEfficientComparisonFunctionPtr validfun,
                        const char *dictName,   /*e.g. MD5\0 */
                        int numEntriesInHashTable, int nPairsPerBlock, int nBytesPerKey,
                        int umUnrelatedBytesToAllocate) {

	char keyFileName[80] = {0}, dataFileName[80] = {0};
	FILE* key_file, *data_file;
	int ret_val;
	DEHT* deht;
	int num_of_seeds = 0;

	strcat(keyFileName, prefix);
	strcat(keyFileName, ".key");
	strcat(dataFileName, prefix);
	strcat(dataFileName, ".data");

	/* we've decided to move responsibility for checking for existence of the files
	 * to the client code. see check_deht_files_exists in global_auth.c
	 */
	key_file = fopen(keyFileName, "w+");
	data_file = fopen(dataFileName, "w+");

	deht = (DEHT*)calloc(1, sizeof(DEHT));
	if (deht == 0)
		return 0;

	deht->anLastBlockSize = 0;
	deht->comparisonHashFunc = validfun;
	deht->dataFP = data_file;
	deht->keyFP = key_file;
	deht->hashFunc = hashfun;
	deht->hashPointersForLastBlockImageInMemory = 0;
	deht->hashTableOfPointersImageInMemory = 0;

	strcpy(deht->header.sDictionaryName, dictName);
	deht->header.numEntriesInHashTable = numEntriesInHashTable;
	deht->header.nPairsPerBlock = nPairsPerBlock;
	deht->header.nBytesPerValidationKey = nBytesPerKey;
	deht->header.numUnrelatedBytesSaved = umUnrelatedBytesToAllocate;

	strcpy(deht->sDatafileName, dataFileName);
	strcpy(deht->sKeyfileName, keyFileName);

	ret_val = fwrite(&deht->header, sizeof(deht->header), 1, key_file);
	if (ret_val != 1) {
		return 0;
	}

	/* write the unrelated bytes */
	num_of_seeds = deht->header.numUnrelatedBytesSaved / sizeof(int64);
	write_seeds(deht, deht->keyFP, num_of_seeds);

	/* write the buckets table */
	ret_val = write_null_bytes(deht->header.numEntriesInHashTable * sizeof(DEHT_DISK_PTR), key_file);
	if (ret_val == DEHT_STATUS_FAIL) {
		return 0;
	}

	flush_DEHT_files(deht);
	return deht;
}


DEHT *load_DEHT_from_files(const char *prefix,
						   hashKeyIntoTableFunctionPtr hashfun, hashKeyforEfficientComparisonFunctionPtr validfun) {
	char keyFileName[80] = {0}, dataFileName[80] = {0};
	FILE* key_file, *data_file;
	DEHT* deht;

	strcat(keyFileName, prefix);
	strcat(keyFileName, ".key");
	strcat(dataFileName, prefix);
	strcat(dataFileName, ".data");

	key_file = fopen(keyFileName, "r");
	if (!key_file) {
		fprintf(stderr, "error! the file %s does not exist\n", keyFileName);
		return 0;
	} else {
		fclose(key_file);
	}

	data_file = fopen(dataFileName, "r");
	if (!key_file) {
		fprintf(stderr, "error! the file %s does not exist\n", dataFileName);
		return 0;
	} else {
		fclose(data_file);
	}

	key_file = fopen(keyFileName, "r+");
	data_file = fopen(dataFileName, "r+");
	if (key_file == 0 || data_file == 0) {
		perror(0);
		return 0;
	}

	deht = (DEHT*)calloc(1, sizeof(DEHT));
	deht->anLastBlockSize = 0;

	strcpy(deht->sDatafileName, dataFileName);
	strcpy(deht->sKeyfileName, keyFileName);

	deht->comparisonHashFunc = validfun;
	deht->dataFP = data_file;
	deht->keyFP = key_file;
	deht->hashFunc = hashfun;
	deht->hashPointersForLastBlockImageInMemory = 0;
	deht->hashTableOfPointersImageInMemory = 0;


	fread(&deht->header, sizeof(DEHTpreferences), 1, deht->keyFP);
	read_seeds(deht);

	return deht;
}



DEHT_DISK_PTR get_bucket_number_k(DEHT* deht, int k) {
	if (deht->hashTableOfPointersImageInMemory == 0) {
		DEHT_DISK_PTR returned_value;
		fseek(deht->keyFP, start_of_ptr_table(deht) + k * sizeof(DEHT_DISK_PTR), SEEK_SET);
		fread(&returned_value, sizeof(DEHT_DISK_PTR), 1, deht->keyFP);
		return returned_value;
	}
	else return deht->hashTableOfPointersImageInMemory[k];
}

void write_bucket_number_k(DEHT* deht, int k, DEHT_DISK_PTR new_bucket) {
	if (deht->hashTableOfPointersImageInMemory == 0) {
		fseek(deht->keyFP, start_of_ptr_table(deht) + k * sizeof(DEHT_DISK_PTR), SEEK_SET);
		fwrite(&new_bucket, sizeof(DEHT_DISK_PTR), 1, deht->keyFP);
	} else {
		deht->hashTableOfPointersImageInMemory[k] = new_bucket;
	}

	if (deht->hashPointersForLastBlockImageInMemory != 0) {
		deht->hashPointersForLastBlockImageInMemory[k] = 0;
		deht->anLastBlockSize[k] = 0;
	}

}

DEHT_DISK_PTR allocate_new_bucket(DEHT* deht) {
	DEHT_DISK_PTR pos;
	int status;
	fseek(deht->keyFP, 0, SEEK_END);
	pos = ftell(deht->keyFP);

	/* write null data to the file. bucket size = record size * records + next pointer size */
	status = write_null_bytes(sizeof(KeyFileRecord) * deht->header.nPairsPerBlock + sizeof(DEHT_DISK_PTR), deht->keyFP);

	if (status != DEHT_STATUS_SUCCESS) {
		perror("error in allocate_new_bucket");
		return 0;
	}

	flush_DEHT_files(deht);

	return pos;

}

/* returns a pointer to the next empty record in the bucket
 * creates a new bucket if the last bucket is empty
 * assumes it is only called by add_DEHT! */

DEHT_DISK_PTR find_empty_place_in_deht(DEHT* deht, int bucket_number) {
	int i;
	DEHT_DISK_PTR next_bucket;
	DEHT_DISK_PTR bucket;

	if (deht->hashPointersForLastBlockImageInMemory != 0) {
		int index_in_bucket;
		bucket = deht->hashPointersForLastBlockImageInMemory[bucket_number];
		index_in_bucket = deht->anLastBlockSize[bucket_number];

		if (bucket == 0) {
			next_bucket = allocate_new_bucket(deht);
			if (next_bucket == 0) {
				return 0;
			}
			write_bucket_number_k(deht, bucket_number, next_bucket);
			deht->hashPointersForLastBlockImageInMemory[bucket_number] = next_bucket;
			deht->anLastBlockSize[bucket_number] = 0;
			return next_bucket;
		}

		if (index_in_bucket != deht->header.nPairsPerBlock) {
			deht->anLastBlockSize[bucket_number] += 1;
			return bucket + index_in_bucket * sizeof(KeyFileRecord);
		}

		/* else (if the last block is full) */
		next_bucket = allocate_new_bucket(deht);
		if (next_bucket == 0) {
			return 0;
		}
		fseek(deht->keyFP, bucket + deht->header.nPairsPerBlock * sizeof(KeyFileRecord), SEEK_SET);
		fwrite(&next_bucket, sizeof(DEHT_DISK_PTR), 1, deht->keyFP);
		deht->hashPointersForLastBlockImageInMemory[bucket_number] = next_bucket;
		deht->anLastBlockSize[bucket_number] = 0;

		flush_DEHT_files(deht);
		return next_bucket;
	}

	else {
		bucket = get_bucket_number_k(deht, bucket_number);

		if (bucket == 0) {
			bucket = allocate_new_bucket(deht);
			if (bucket == 0) {
				return 0;
			}
			write_bucket_number_k(deht, bucket_number, bucket);
			return bucket;
		}

		while (true) {
		fseek(deht->keyFP, bucket, SEEK_SET);
			for (i = 0; i < deht->header.nPairsPerBlock; i++) {
				KeyFileRecord temp_record;
				fread(&temp_record, sizeof(KeyFileRecord), 1, deht->keyFP);
				if (temp_record.validity == 1) {
					continue;
				}
				else {
					fseek(deht->keyFP, -sizeof(KeyFileRecord), SEEK_CUR);
					return ftell(deht->keyFP);
				}
			}
			/*if not found*/
			fread(&next_bucket, sizeof(DEHT_DISK_PTR), 1, deht->keyFP);
			if (next_bucket == 0) {
				next_bucket = allocate_new_bucket(deht);
				if (next_bucket == 0) {
					return 0;
				}
				fseek(deht->keyFP, bucket + deht->header.nPairsPerBlock*sizeof(KeyFileRecord), SEEK_SET);
				fwrite(&next_bucket, sizeof(DEHT_DISK_PTR), 1, deht->keyFP);
				flush_DEHT_files(deht);
				return next_bucket;
			}
			else {
				bucket = next_bucket; /* and back to the while(true) up there */
				continue;
			}
		}
	}
}

/* adds the data to the end of the data file.
 * returns its position in the data file.
 */
DEHT_DISK_PTR insert_data(DEHT* deht, const unsigned char *data, int dataLength) {
	DEHT_DISK_PTR end_of_file;
	fseek(deht->dataFP, 0, SEEK_END);
	end_of_file = ftell(deht->dataFP);
	fwrite(data, 1, dataLength, deht->dataFP);
	return end_of_file;
}

int add_DEHT ( DEHT *deht, const unsigned char *key, int keyLength,
				 const unsigned char *data, int dataLength) {
	KeyFileRecord record;
	DEHT_DISK_PTR place_to_write = 0;
	DEHT_DISK_PTR insert_return_value;
	int hash_value = deht->hashFunc(key, keyLength, deht->header.numEntriesInHashTable);

	place_to_write = find_empty_place_in_deht(deht, hash_value);
	if (place_to_write == 0) {
		return DEHT_STATUS_FAIL;
	}

	deht->comparisonHashFunc(key, keyLength, record.key_signature);
	/* assumes nBytesPerValidationKey <= 16! */
	insert_return_value = insert_data(deht, data, dataLength);
	record.offset_in_data_file_low = (int)insert_return_value;
	record.offset_in_data_file_high = insert_return_value >> 32;
	record.size_in_data = dataLength;
	record.validity = 1;
	fseek(deht->keyFP, place_to_write, SEEK_SET);
	fwrite(&record, sizeof(KeyFileRecord), 1, deht->keyFP);

	if (deht->hashPointersForLastBlockImageInMemory != 0) {
		deht->anLastBlockSize[hash_value] += 1;
	}

	flush_DEHT_files(deht);
	return DEHT_STATUS_SUCCESS;
}

int str_mod( const unsigned char *str, int length, int p) {
	int result = 0;
	int i;
	unsigned char c;
	for (i = 0; i < length; i++) {
		c = str[i];
		result = ((result*256)+c)%p;
	}

	return result;
}

/* copies top 8 bytes of the md5sum of the key to output */
int keyTo64Bit(const unsigned char *key,int key_length, unsigned char *output) {
	unsigned char md5_of_key[MD5_OUTPUT_LENGTH_IN_BYTES];
	MD5BasicHash(key, key_length, md5_of_key);
	memcpy(output, md5_of_key, 8);
	return 0;
}


/* r is an output parameter
 * the function will return a disk pointer to a record with the same key and the record itself (in r) if it is found,
 * and 0 otherwise */
DEHT_DISK_PTR searchForKey_DEHT(DEHT* deht, const unsigned char* key, int keyLength, KeyFileRecord* r, DEHT_DISK_PTR bucket) {
	unsigned char key_signature[SIGNATURE_SIZE];
	int i;
	deht->comparisonHashFunc(key, keyLength, key_signature);

	while (true) {
	fseek(deht->keyFP, bucket, SEEK_SET);
		for (i = 0; i < deht->header.nPairsPerBlock; i++) {
			KeyFileRecord temp_record;
			DEHT_DISK_PTR current_record_pointer = ftell(deht->keyFP);
			fread(&temp_record, sizeof(KeyFileRecord), 1, deht->keyFP);
			if (temp_record.validity == 1 && !memcmp(temp_record.key_signature, key_signature, SIGNATURE_SIZE)) {
				*r = temp_record;
				return current_record_pointer;
			}
		}
		/*if not found in this block*/
		fread(&bucket, sizeof(DEHT_DISK_PTR), 1, deht->keyFP);
		if (bucket == 0) {
			return 0;
		}
		else {
			continue; /*with bucket == next bucket in list*/
		}
	}


}


int query_DEHT ( DEHT *deht, const unsigned char *key, int keyLength,
				 unsigned char *data, int dataMaxAllowedLength) {

	int hash_value;
	DEHT_DISK_PTR bucket;
	KeyFileRecord record;
	DEHT_DISK_PTR returned_value;
	int bytes_to_copy;
	hash_value = deht->hashFunc(key, keyLength, deht->header.numEntriesInHashTable);
	bucket = get_bucket_number_k(deht, hash_value);

	if (bucket == 0) {
		return DEHT_STATUS_NOT_NEEDED;
	}

	returned_value = searchForKey_DEHT(deht, key, keyLength, &record, bucket);
	if (returned_value == 0)
		return DEHT_STATUS_NOT_NEEDED;

	bytes_to_copy = min_int(dataMaxAllowedLength, record.size_in_data);

	fseek(deht->dataFP, ((DEHT_DISK_PTR)record.offset_in_data_file_high << 32) | record.offset_in_data_file_low, SEEK_SET);
	fread(data, 1, bytes_to_copy, deht->dataFP);

	return bytes_to_copy;
}


int insert_uniquely_DEHT ( DEHT *ht, const unsigned char *key, int keyLength,
				 const unsigned char *data, int dataLength) {
	int hash_value;
	DEHT_DISK_PTR bucket;
	DEHT_DISK_PTR insert_return_value;
	KeyFileRecord record;
	int written_size;
	int returned_value;
	hash_value = ht->hashFunc(key, keyLength, ht->header.numEntriesInHashTable);
	bucket = get_bucket_number_k(ht, hash_value);

	returned_value = searchForKey_DEHT(ht, key, keyLength, &record, bucket);
	if (returned_value == 0) {
		return add_DEHT(ht, key, keyLength, data, dataLength);
	} else {
		insert_return_value = insert_data(ht, data, dataLength);/* insert the data again */
		record.offset_in_data_file_low = (int)insert_return_value;
		record.offset_in_data_file_high = (DEHT_DISK_PTR)insert_return_value >> 32;
		record.size_in_data = dataLength;
		fseek(ht->keyFP, returned_value, SEEK_SET);
		written_size = fwrite(&record, sizeof(KeyFileRecord), 1, ht->keyFP);
		if (written_size != sizeof(KeyFileRecord)) {
			return DEHT_STATUS_FAIL;
		}
		flush_DEHT_files(ht);
		return DEHT_STATUS_NOT_NEEDED;
	}

}

int read_DEHT_pointers_table(DEHT *deht) {
	int bytes_read;
	if (deht->hashTableOfPointersImageInMemory != 0) {
		return DEHT_STATUS_NOT_NEEDED;
	}
	deht->hashTableOfPointersImageInMemory = (DEHT_DISK_PTR*)malloc(sizeof(DEHT_DISK_PTR) * deht->header.numEntriesInHashTable);
	fseek(deht->keyFP, start_of_ptr_table(deht), SEEK_SET); /* go to the beginning of the pointer table */

	bytes_read = fread(deht->hashTableOfPointersImageInMemory, sizeof(DEHT_DISK_PTR), deht->header.numEntriesInHashTable, deht->keyFP);
	if (bytes_read != deht->header.numEntriesInHashTable * sizeof(DEHT_DISK_PTR)) {
		return DEHT_STATUS_FAIL;
	}

	return DEHT_STATUS_SUCCESS;
}

int write_DEHT_pointers_table(DEHT *deht) {
	int bytes_written;

	if (deht->hashTableOfPointersImageInMemory == 0) {
		return DEHT_STATUS_NOT_NEEDED;
	}

	fseek(deht->keyFP, start_of_ptr_table(deht), SEEK_SET);
	bytes_written = fwrite(deht->hashTableOfPointersImageInMemory, sizeof(DEHT_DISK_PTR), deht->header.numEntriesInHashTable, deht->keyFP);

	free(deht->hashTableOfPointersImageInMemory);

	if (bytes_written != sizeof(DEHT_DISK_PTR) * deht->header.numEntriesInHashTable) {
		return DEHT_STATUS_FAIL;
	}

	return DEHT_STATUS_SUCCESS;
}


void get_last_block_and_index_for_bucket(DEHT* deht, int bucket_number, DEHT_DISK_PTR* out_block_address, int* out_index_in_block) {
	DEHT_DISK_PTR bucket_address, current_block_address, next_block_address;
	int i;

	bucket_address = get_bucket_number_k(deht, bucket_number);
	current_block_address = bucket_address;

	if (bucket_address == 0) {
		*out_block_address = 0;
		*out_index_in_block = deht->header.nPairsPerBlock;
		return;
	}

	while (true) { /* continue until the last block address has been found */
		fseek(deht->keyFP, current_block_address, SEEK_SET);

		for (i = 0; i < deht->header.nPairsPerBlock; i++) {
			KeyFileRecord temp_record;
			fread(&temp_record, sizeof(KeyFileRecord), 1, deht->keyFP);
			if (temp_record.validity == 1) {
				continue;
			} else { /* found an empty place in the block */
				*out_block_address = current_block_address;
				*out_index_in_block = i;
				return;
			}
		}

		fread(&next_block_address, sizeof(DEHT_DISK_PTR), 1, deht->keyFP);

		if (next_block_address == 0) { /* add the current block to the table even though it is full */
			*out_block_address = current_block_address;
			*out_index_in_block  = deht->header.nPairsPerBlock;
			return;
		}
		else {
			current_block_address = next_block_address;
			continue;
		}
	}
}

/* populates deht->hashPointersForLastBlockImageInMemory with the pointers to the last block in each bucket.
 * if the last block is full, it still returns the last block.
 *
 * populates deht->anLastBlockSize, for each bucket, with the index of the first empty place in the last
 * block in the bucket. If the last block is full, the value will be deht->header.nPairsPerBlock.
 */
int calc_DEHT_last_block_per_bucket(DEHT *deht) {
	int bucket_number, index_in_block;
	DEHT_DISK_PTR block_address;

	if (deht->hashPointersForLastBlockImageInMemory != 0)
		return DEHT_STATUS_NOT_NEEDED;

	deht->hashPointersForLastBlockImageInMemory = (DEHT_DISK_PTR*)calloc(sizeof(DEHT_DISK_PTR), deht->header.numEntriesInHashTable);
	deht->anLastBlockSize = (int*)calloc(sizeof(int), deht->header.numEntriesInHashTable);

	if (deht->hashPointersForLastBlockImageInMemory == 0 || deht->anLastBlockSize == 0)
		return DEHT_STATUS_FAIL;

	for (bucket_number = 0; bucket_number < deht->header.numEntriesInHashTable; bucket_number++) {
		get_last_block_and_index_for_bucket(deht, bucket_number, &block_address, &index_in_block);
		deht->anLastBlockSize[bucket_number] = index_in_block;
		deht->hashPointersForLastBlockImageInMemory[bucket_number] = block_address;
	}

	return DEHT_STATUS_SUCCESS;
}


static void flush_DEHT_files(DEHT* deht) {
	fflush(deht->dataFP);
	fflush(deht->keyFP);
}


void lock_DEHT_files(DEHT *deht) {
	if (deht->hashTableOfPointersImageInMemory != 0) {
		write_DEHT_pointers_table(deht);
	}
	if (deht->seeds != 0) {
		free(deht->seeds);
		deht->seeds = 0;
	}

	if (deht->hashPointersForLastBlockImageInMemory != 0) {
		free(deht->hashPointersForLastBlockImageInMemory);
		deht->hashPointersForLastBlockImageInMemory = 0;
	}
	if (deht->anLastBlockSize != 0) {
		free(deht->anLastBlockSize);
		deht->anLastBlockSize = 0;
	}


	fclose(deht->dataFP);
	fclose(deht->keyFP);
}

/* takes a DEHT and a key, and returns all values added for that key.
 * output parameters: out_array is an array of String pointers, out_array_size is the length of out_array.
 * used in crack_using_rainbow_table.
 */
void retrieve_multiple_values_from_DEHT(DEHT* d, unsigned char* key, int key_size, String** out_array, int* out_array_size) {
	int hash_value;
	DEHT_DISK_PTR bucket;
	int array_size = 0;
	String* string_array = 0;
	unsigned char data[40] = {0};
	unsigned char key_signature[SIGNATURE_SIZE];
	int i;

	hash_value = d->hashFunc(key, key_size, d->header.numEntriesInHashTable);
	bucket = get_bucket_number_k(d, hash_value);

	if (bucket == 0) {
		*out_array = 0;
		*out_array_size = 0;
		return;
	}


	d->comparisonHashFunc(key, key_size, key_signature);

	/* iterate all KeyFileRecords in the bucket - similar to query_DEHT*/
	while (true) {
	fseek(d->keyFP, bucket, SEEK_SET);
		for (i = 0; i < d->header.nPairsPerBlock; i++) {
			KeyFileRecord temp_record;
			fread(&temp_record, sizeof(KeyFileRecord), 1, d->keyFP);
			if (temp_record.validity == 1 && !memcmp(temp_record.key_signature, key_signature, SIGNATURE_SIZE)) {
				/* found a match - insert it into string_array (and return it later)*/
				fseek(d->dataFP, ((DEHT_DISK_PTR)temp_record.offset_in_data_file_high << 32) | temp_record.offset_in_data_file_low, SEEK_SET);
				fread(data, 1, temp_record.size_in_data, d->dataFP);

				/* enlarge string_array and write to its end */
				string_array = realloc(string_array,(++array_size) * sizeof(String));
				String_init(&string_array[array_size - 1], temp_record.size_in_data);
				memcpy(string_array[array_size - 1].data, data, temp_record.size_in_data);
				string_array[array_size - 1].size = temp_record.size_in_data;
			}
		}

		fread(&bucket, sizeof(DEHT_DISK_PTR), 1, d->keyFP);
		if (bucket == 0) {
			break;
		}
		else {
			continue; /*with bucket == next bucket in list*/
		}
	}

	if (array_size > 0) {
		*out_array = string_array;
		*out_array_size = array_size;
	} else {
		*out_array = 0;
		*out_array_size = 0;
	}

}
